<?php
/**
 * Copyright 2011	SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.	If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */
?>
<?php
require_once("escape/saml/include.php");
	
/**
 * Process SAML login
 * 
 * Process a SAML login, redirecting either to the return URL or home page.
 */
class escape_repositoryui_action_ProcessSamlLogin extends escape_repositoryui_Action
{
	public function handleRequest()
	{
		/* @var $repository escape_repository_Repository */
		$repository =& $this->requestAttributes['repository'];
		/* @var $repositoryUi escape_repositoryui_RepositoryUi */
		$repositoryUi =& $this->requestAttributes['repositoryUi'];

		// check if SAML is enabled
		if(!$repository->config['saml']['enabled'])
		{
			// openid is disabled
			$repositoryUi->displayErrorPage('SAML is disabled.');
			return null;
		}
		
		// fetch the return URL from the relay state field in the SAML response POST
		$returnUrl = $_POST['RelayState'];
		
		// switch on SAML debug mode if the string 'debug' is provided in the return URL field
		$samlDebug = false;
		if($returnUrl == 'debug')
		{
			$samlDebug = true;
		}
		
		// use the home URL if no return URL was provided
		$homeUrl = $repository->config['server_url'] . '/';
		if(!$returnUrl)
		{
			$returnUrl = $homeUrl;
		}

		// generate a SamlSettings object based on the ESCAPE configuration
		$samlSettings = new escape_saml_SamlSettings($repository->config);
		
		// process the SAML response
		$samlResponse = new SamlResponse($samlSettings, $_POST['SAMLResponse']);
		
		//TODO: move most of this code to a separate class (in the escape/saml pacakge)?
		
		// validate the SAML response
		$isValid = false;
		try {
			$isValid = $samlResponse->is_valid();
		}
		catch (Exception $e) {
			$isValid = false;
		}
		
		if(!$isValid)
		{
			// SAML validation failed
			$repositoryUi->displayErrorPage('Failed to validate the SAML response.');
			return null;
		}

		// fetch the SAML attributes
		$samlAttributes = $samlResponse->get_attributes();
		
		// ID mapping
		$userId = $samlResponse->get_nameid();
		if($repository->config['saml']['id_mapping'])
		{
			foreach($repository->config['saml']['id_mapping'] as $samlAttributeName)
			{
				if(isset($samlAttributes[$samlAttributeName]))
				{
					// always pick the first value
					$userId = $samlAttributes[$samlAttributeName][0];
				}
			}
		}
		
		// ID translation
		if($repository->config['saml']['id_translation'])
		{
			foreach($repository->config['saml']['id_translation'] as $translationArray)
			{
				$pattern = $translationArray['pattern'];
				$replacement = $translationArray['replacement'];
				
				$userId = preg_replace($pattern, $replacement, $userId);
			}
		}
		
		if(!$userId)
		{
			$repositoryUi->displayErrorPage('Missing user ID in SAML response.');
			return null;
		}
		
		// attribute mapping
		$userProperties = array();
		foreach($repository->config['saml']['attribute_mappings'] as $userPropertyUri => $samlAttributeNames)
		{
			foreach($samlAttributeNames as $samlAttributeName)
			{
				if(isset($samlAttributes[$samlAttributeName]))
				{
					$userProperties[$userPropertyUri] = $samlAttributes[$samlAttributeName];
					break;
				}
			}
		}

		// check if all necessary attributes were provided
		if(!$userProperties[escape_repository_User::$predicate_name][0])
		{
			$repositoryUi->displayErrorPage('Missing user name in SAML response.');
			return null;
		}
		if(!$userProperties[escape_repository_User::$predicate_mbox][0])
		{
			$repositoryUi->displayErrorPage('Missing user email address in SAML response.');
			return null;
		}
		
		// role mapping
		$roleUris = null;
		$roleMappingEnabled = count($repository->config['saml']['role_mappings']) > 0;
		if($roleMappingEnabled)
		{
			$roleUris = array();
			// loop over the role URIs
			foreach($repository->config['saml']['role_mappings'] as $roleUri => $samlAttributePatterns)
			{
				// loop over the attribute match patterns
				foreach($samlAttributePatterns as $samlAttributeName => $samlAttributePattern)
				{
					// check if the pattern is a regex or not
					$isRegex = preg_match('~^/~', $samlAttributePattern);
					
					// check if the attribute to match exists in the SAML attributes
					if(isset($samlAttributes[$samlAttributeName]))
					{
						// loop over all SAML attribute values
						foreach($samlAttributes[$samlAttributeName] as $samlAttributeValue)
						{
							// check if the attribute value matches the pattern
							if(!$isRegex && $samlAttributePattern == $samlAttributeValue)
							{
								// string matched
								$roleUris[] = $roleUri;
								break 2;
							}
							else if($isRegex && preg_match($samlAttributePattern, $samlAttributeValue))
							{
								// regex matched
								$roleUris[] = $roleUri;
								break 2;
							}
						}
					}
				}
			}
		}
		
		// add the matched roles to the user properties
		if(count($roleUris) > 0)
		{
			$userProperties[escape_repository_User::$predicate_system_role] = $roleUris;
		}

		// if SAML debug mode is enabled, show the debug information instead of processing
		if($samlDebug)
		{
			$this->requestAttributes['samlResponse'] = $samlResponse;
			$this->requestAttributes['userId'] = $userId;
			$this->requestAttributes['userProperties'] = $userProperties;
			
			return 'SamlDebugShow';
		}

		try
		{
			$userUri = $repository->convertUserObjectIdToObjectUri($userId);
		}
		catch(escape_repository_exception_InvalidUri $exception)
		{
			$repositoryUi->displayErrorPage("Invalid characters found in the user ID '" . $userId . "', please check the SAML ID translation setting.");
			return null;
		}

		// fetch the user object, if it already exists
		$user = null;
		try
		{
			$user = $repository->getUserByUri($userUri);
			
			if($user !== null)
			{
				// if the user object exists, check if it has the correct data source identifier
				if($user->getDataSource() !== $repository->config['saml']['dataSource'])
				{
					$repositoryUi->displayErrorPage('User already exist and has a different data source identifier.');
					return null;
				}
			}
		}
		catch(fedora_exception_InvalidFedoraPid $exception)
		{
			//ignore
		}
		catch(escape_repository_exception_NotFound $exception)
		{
			//ignore
		}
		
		// if the user object doesn't exist, create it
		if($user === null)
		{
			$user = $repository->createUser($userUri);
			
			// set the data source identifier
			$user->setDataSource($repository->config['saml']['dataSource']);
		}
		
		// get the current user properties
		$currentUserProperties = $user->getProperties();
		// clone (not by reference!) the current user properties
		$updatedUserProperties = unserialize(serialize($currentUserProperties));
		
		// replace the mandatory properties
		$updatedUserProperties->removeStatement(escape_repository_User::$predicate_name);
		$updatedUserProperties->addStatement(escape_repository_User::$predicate_name, $userProperties[escape_repository_User::$predicate_name][0]);
		$updatedUserProperties->removeStatement(escape_repository_User::$predicate_mbox);
		$updatedUserProperties->addStatement(escape_repository_User::$predicate_mbox, $userProperties[escape_repository_User::$predicate_mbox][0]);

		// replace roles only if role overwrite is enabled
		if($repository->config['saml']['role_overwrite'])
		{
			$updatedUserProperties->removeStatement(escape_repository_User::$predicate_system_role);
		}
		// add matched roles
		$updatedUserProperties->addStatement(escape_repository_User::$predicate_system_role, $userProperties[escape_repository_User::$predicate_system_role]);
		
		// save the updated user properties only if there are changes 
		if($currentUserProperties != $updatedUserProperties)
		{
			$user->setProperties($updatedUserProperties);
		}

		// overwrite optional properties only if set in the SAML attributes
		if($userProperties[escape_repository_User::$predicate_organization][0] != '')
		{
			$user->setOrganization($userProperties[escape_repository_User::$predicate_organization][0]);
		}
		if($userProperties[escape_repository_User::$predicate_dai][0] != '')
		{
			$user->setDai($userProperties[escape_repository_User::$predicate_dai][0]);
		}
		
		$_SESSION['user_uri'] = $user->getUri();
		header("Location: " . $returnUrl);
		
		return null;
	}
}